#!/usr/bin/env python

__description__ = 'JPEG file analysis tool'
__author__ = 'Didier Stevens'
__version__ = '0.0.7'
__date__ = '2019/04/27'

"""
Source code put in public domain by Didier Stevens, no Copyright
https://DidierStevens.com
Use at your own risk

History:
  2017/09/09: start
  2017/09/10: continue
  2017/09/12: 0.0.2 reporting missing bytes
  2017/09/19: 0.0.3 refactoring; added option -f
  2017/11/01: updated cDump
  2017/12/01: updated FilenameCheckHash to handle empty file: #
  2017/12/18: refactoring; man page
  2018/01/28: added option -c
  2018/01/30: 0.0.4 added option -e
  2018/03/05: 0.0.5 updated #e# expressions
  2018/06/12: man updated
  2018/06/17: 0.0.6 added property extracted to cBinaryFile
  2019/04/20: 0.0.7 added option -t
  2019/04/27: CutData UNICODE & Find; added option -A

Todo:
"""

import optparse
import sys
import os
import zipfile
import binascii
import random
import gzip
import collections
import glob
import textwrap
import re
import struct
import string
import math
if sys.version_info[0] >= 3:
    from io import BytesIO as DataIO
else:
    from cStringIO import StringIO as DataIO
if sys.version_info[0] >= 3:
    from io import StringIO
else:
    from cStringIO import StringIO

def PrintManual():
    manual = r'''
Manual:

The JPEG file format defines a JPEG file as a sequence of segments. Each segments begins with a 2-byte marker. The first byte is always 0xFF, and the second byte specifies the type of marker. Most markers are followed by 2 bytes indicating the length of the data (payload) that follows. Some marker have no data.
Start Of Image (SOI: 0xFFD8), End Of Image (EOI: 0xFFD9) and Restart (RST: 0xFF00 - 0xFF07) markers have no data.

jpegdump.py is a tool that takes one or more files to analyze them for sequences. It does this by searching for 0xFF bytes and then starts to parse them as markers (possibly followed by data).
This tool is very versatile when it comes to handling files, later full details will be provided.

This Python script was developed with Python 2.7 and tested with Python 2.7 and 3.5.

Here is an example for image j.jpg:

jpegdump.py j.jpg

File: j.jpg
  1 p=0x00000000    : m=ffd8 SOI
  2 p=0x00000002 d=0: m=ffe1 APP1  l=64837 e=6.886123 a=61.043573
  3 p=0x0000fd49 d=0: m=ffdb DQT   l=  197 e=2.343628 a=0.618557 remark: 195/65 = 3.000000
  4 p=0x0000fe10 d=0: m=ffc4 DHT   l=  418 e=7.009735 a=14.149398
  5 p=0x0000ffb4 d=0: m=ffc0 SOF0  l=   17 e=3.189898 a=27.357143 remark: p=8 h=3456 w=4608 c=3
  6 p=0x0000ffc7 d=0: m=ffda SOS   l=   12 e=2.446439 a=21.222222
                                  entropy-coded data: l=3529048 e=7.983918 a=84.905246 #ff00=8681
  7 p=0x0036d92d d=0: m=ffd9 EOI

Each line represents the details of a segment. The first number, the index, is a sequential number generated by jpegdump and used to select a segment for further analysis.
The second field (p=) is the position field: it gives the position of the marker inside the file, expressed as a hexadecimal number.
The third field (d=) is the difference field: it gives the number of bytes between this sequence and the previous sequence, expressed as a decimal number. This value should be 0 for well-formed jpeg files.
The fourth field (m=) is the marker field: 2 hexadecimal bytes (0xFF??) indicating the type of the sequence followed by an acronym of the sequency type. This acronym can be:

SOI:  Start Of Image
SOF0: Start Of Frame (baseline DCT)
SOF2: Start Of Frame (progressive DCT)
DHT:  Define Huffman Table
DQT:  Define Quantization Table(s)
DRI:  Define Restart Interval
SOS:  Start Of Scan
RST?: Restart
APP?: Application
COM:  Comment
EOI:  End Of Image

This information is followed by statistics for the data of the sequence (SOI, EOI and RST sequences have no data):
l= is the length of the data
e= is the entropy of the data
a= is the "average of the absolute difference between 2 consecutive bytes"

Finally, this can be followed by a remark specific for the sequence.
DQT: the number of tables
SOF0: the precision in bits (p=), height in pixel (h=), width in pixels (w=) and number of color components (c=).
SOS: the number of color components (c=).

A SOS segment's data is followed by the image data. The statistics of the image data are displayed on the second line. Image data can contain 0xFF bytes, which could be mistaken for markers. To clearly indicate that 0xFF bytes inside image data are not markers, 0xFF bytes are always followed by 0x00 (byte stuffing). This is counted by value #ff00=.

If the last segment is incomplete, a *warning* line will be displayed.
If the last segment is followed by extra bytes, a *trailing* line will be displayed.
Use option -t if you want to consider all bytes after the first EOI marker as trailing (e.g. without being parsed).

Segments can be selected to inspect their data. This can be done with option -s and the index number.
Example:

jpegdump.py -s 3 j.jpg

0000FD4D: 00 02 01 01 01 01 01 02  01 01 01 02 02 02 02 02  ................
0000FD5D: 04 03 02 02 02 02 05 04  04 03 04 06 05 06 06 06  ................
0000FD6D: 05 06 06 06 07 09 08 06  07 09 07 06 06 08 0B 08  ................
0000FD7D: 09 0A 0A 0A 0A 0A 06 08  0B 0C 0B 0A 0C 09 0A 0A  ................
0000FD8D: 0A 01 02 02 02 02 02 02  05 03 03 05 0A 07 06 07  ................
0000FD9D: 0A 0A 0A 0A 0A 0A 0A 0A  0A 0A 0A 0A 0A 0A 0A 0A  ................
0000FDAD: 0A 0A 0A 0A 0A 0A 0A 0A  0A 0A 0A 0A 0A 0A 0A 0A  ................
0000FDBD: 0A 0A 0A 0A 0A 0A 0A 0A  0A 0A 0A 0A 0A 0A 0A 0A  ................
0000FDCD: 0A 0A 02 02 02 02 02 02  02 05 03 03 05 0A 07 06  ................
0000FDDD: 07 0A 0A 0A 0A 0A 0A 0A  0A 0A 0A 0A 0A 0A 0A 0A  ................
0000FDED: 0A 0A 0A 0A 0A 0A 0A 0A  0A 0A 0A 0A 0A 0A 0A 0A  ................
0000FDFD: 0A 0A 0A 0A 0A 0A 0A 0A  0A 0A 0A 0A 0A 0A 0A 0A  ................
0000FE0D: 0A 0A 0A                                          ...

When an SOS segment is selected, the segment's data will be displayed:

jpegdump.py -s 6 j.jpg

0000FFCB: 03 01 00 02 11 03 11 00  3F 00                    ........?.

To display the image data, append the indexnumber with letter i:

jpegdump.py -s 6i j.jpg

      0000FFD5: F8 71 A1 37 30 97 45 FB  DD 14 8E 94 D8 A1 4B 08  .q.70.E.......K.
      0000FFE5: 09 91 30 4F 3C FA 57 E6  D2 7A 1F B3 47 71 00 1B  ..0O<.W..z..Gq..
      0000FFF5: 03 A3 9D B9 CB 63 AD 4D  25 F3 46 A3 C8 C9 E3 81  .....c.M%.F.....
      00010005: 59 B4 74 44 7F 9C 7C B5  71 1E 37 2F CD 9E C6 98  Y.tD.|.q.7/....
      00010015: F3 B3 61 42 13 8E E2 92  56 2E FA 10 4E F2 DF CA  ..aB....V...N...
      00010025: 86 D7 74 61 78 C6 3A 9F  5A D3 B3 92 68 86 64 04  ..tax.:.Z...h.d.
      00010035: 81 C7 35 5B 08 9E 58 56  E4 03 BC 2E 3A FB D3 A2  ..5[..XV....:...
      00010045: B1 82 10 1E 42 1B 9F BD  50 E4 1C B7 2B 5F 40 D7  ....B...P...+_@.
      00010055: 2C 4B 7A F1 C7 6A 85 6D  DA D1 91 F0 09 FE EB 8E  ,Kz..j.m........
      00010065: A2 AE 32 26 50 08 ED 05  CD C2 8D DB 55 9B 90 2A  ..2&P.......U..*

By default, and hexadecimal/ascii dump of the data dis produced (option -a).
Use option -x for an hexadecimal dump.
Option -A does an ASCII dump (like option -a), but with duplicate lines removed.
Use option -d for a binary dump, and option -u for a binary dump with byte unstuffing (replacing 0xFF00 with 0xFF).

By default, jpegdump will start to analyze the first marker it finds, regardless of its type.
To force jpegdump to search for and start with SOI markers, use option -f. With option -f, analysis will stop when an unknown marker is detected, and the next SOI marker will be searched.
Example:

jpegdump.py -f chrome.dmp
File: chrome.dmp
Found SOI:
  1 p=0x000d4ebf    : m=ffd8 SOI
  2 p=0x000d4ec6 d=5: m=ffff       l=53252 e=3.280114 a=40.576499
Found SOI:
  1 p=0x0018594b    : m=ffd8 SOI
  2 p=0x001859d4 d=135: m=ff03       l=    0 e=0.000000 a=0.000000
...

Option f can output a lot of markers who are actually not part of a JPEG image. To use jpegdump to "carve" memory dumps or documents like PDF files, combine option -f with option -c (compliant). Used together with option -c, jpegdump will only report sequences of markers that are "compliant".
In this context, a compliant image is a sequence of markers that starts with SOI and ends with EOI, and where the difference (d=) between markers is 0.
Example:

jpegdump.py -f -c chrome.dmp
File: chrome.dmp
Found SOI:
  1 p=0x01854e2f    : m=ffd8 SOI
  2 p=0x01854e31 d=0: m=ffd9 EOI
Found SOI:
  1 p=0x02144d5b    : m=ffd8 SOI
  2 p=0x02144d5d d=0: m=ffe1 APP1  l=   24 e=1.945660 a=15.857143
  3 p=0x02144d77 d=0: m=ffec APP12 l=   17 e=2.596792 a=23.857143
  4 p=0x02144d8a d=0: m=ffee APP14 l=   14 e=2.751629 a=49.818182
  5 p=0x02144d9a d=0: m=ffdb DQT   l=  132 e=3.272901 a=1.573643 remark: 130/65 = 2.000000
  6 p=0x02144e20 d=0: m=ffc0 SOF0  l=   17 e=2.872906 a=47.785714 remark: p=8 h=222 w=574 c=3
  7 p=0x02144e33 d=0: m=ffc4 DHT   l=  160 e=4.721659 a=27.662420
  8 p=0x02144ed5 d=0: m=ffda SOS   l=   12 e=2.446439 a=21.222222 remark: c=3
                                  entropy-coded data: l=8481 e=7.528572 a=77.173467 #ff00=22
  9 p=0x02147004 d=0: m=ffd9 EOI
...

Option -e can be used with option -f to extract all found images to disk. Images are written to the current directory with name AAA.jpeg, where AAA is the hexadecimal address of the position of the image in the file.


As stated at the beginning of this manual, this tool is very versatile when it comes to handling files. This will be explained now.

This tool reads files in binary mode. It can read files from disk, from standard input (stdin) and from "generated" files via the command line.
It can also partially read files (this is done with the cut operator).

If no file arguments are provided to this tool, it will read data from standard input (stdin). This way, this tool can be used in a piped chain of commands, like this:

oledump.py -s 4 -d sample.doc.vir | tool.py

When one or more file arguments are provided to this tool, it will read the files and process the content.
How the files are read, depends on the type of file arguments that are provided. File arguments that start with character @ or # have special meaning, and will be explained later.

If a file argument does not start with @ or #, it is considered to be a file on disk and the content will be read from disk.
If the file is not a compressed file, the binary content of the file is read from disk for processing.
Compressed files are solely recognized based on their extension: .zip and .gz.
If a file argument with extension .gz is provided, the tool will decompress the gzip file in memory and process the decompressed content. No checks are made to ensure that the file with extension .gz is an actual gzip compressed file.
If a file argument with extension .zip is provided and it contains a single file, the tool will extract the file from the ZIP file in memory and process the decompressed content. No checks are made to ensure that the file with extension .zip is an actual ZIP compressed file.
Password protected ZIP files can be processed too. The tool uses password 'infected' (without quotes) as default password. A different password can be provided using option --password.

Example:

tool.py sample.zip

To prevent the tool from decompressing .zip or .gz files, but to process the compressed file itself, use option --noextraction.

File arguments that start with character @ ("here files"), are read as text files that contain file arguments (one per line) to be processed.
For example, we take a text file with filename list.txt and following content:

sample-1.bin
sample-5.bin
sample-7.bin

When using this file (list.txt) in the following command:

tool.py @list.txt

the tool will process the following files: sample-1.bin, sample-5.bin and sample-7.bin.
A single @ character as filename is a here file read from stdin.

Wildcards are supported too. The classic *, ? and [] wildcard characters are supported. For example, use the following command to process all .exe and .dll files in the Windows directory:

tool.py C:\Windows\*.exe C:\Windows\*.dll

To prevent the tool from processing file arguments with wildcard characters or special initial characters (@ and #) differently, but to process them as normal files, use option --literalfilenames.

File arguments that start with character # have special meaning. These are not processed as actual files on disk (except when option --literalfilenames is used), but as file arguments that specify how to "generate" the file content.

File arguments that start with #, #h#, #b# or #e# are used to "generate" the file content.
Arguments that start with #c# are not file arguments, but cut operators (explained later).

Generating the file content with a # file argument means that the file content is not read from disk, but generated in memory based on the characteristics provided via the file argument.

When a file argument starts with # (and not with #h#, #b#, #e# or #c#), all characters that follow the # character specify the content of the generated file.
For example, file argument #ABCDE specifies a file containing exactly 5 bytes: ASCII characters A, B, C, D and E.
Thus the following command:

tool.py #ABCDE

will make the tool process data with binary content ABCDE. #ABCDE is not an actual file written on disk, but it is a notational convention to provide data via the command line.

Since this notation can not be used to specify all possible byte values, hexadecimal encoding (#h#) and BASE64 encoding (#b#) notation is supported too.
For example, #h#4142434445 is an hexadecimal notation that generates data ABCDE. Hexadecimal notation allows the generation of non-printable characters for example, like NULL bytes: #h#00
File argument #b#QUJDREU= is another example, this time BASE64 notation, that generates data ABCDE.

File arguments that start with #e# are a notational convention to use expressions to generate data. An expression is a single function/string or the concatenation of several functions/strings (using character + as concatenation operator).
Strings can be characters enclosed by single quotes ('example') or hexadecimal strings prefixed by 0x (0xBEEF).
4 functions are available: random, loremipsum, repeat and chr.

Function random takes exactly one argument: an integer (with value 1 or more). Integers can be specified using decimal notation or hexadecimal notation (prefix 0x).
The random function generates a sequence of bytes with a random value (between 0 and 255), the argument specifies how many bytes need to be generated. Remark that the random number generator that is used is just the Python random number generator, not a cryptographic random number generator.

Example:

tool.py #e#random(100)

will make the tool process data consisting of a sequence of 100 random bytes.

Function loremipsum takes exactly one argument: an integer (with value 1 or more).
The loremipsum function generates "lorem ipsum" text (fake latin), the argument specifies the number of sentences to generate.

Example: #e#loremipsum(2) generates this text:
Ipsum commodo proin pulvinar hac vel nunc dignissim neque eget odio erat magna lorem urna cursus fusce facilisis porttitor congue eleifend taciti. Turpis duis suscipit facilisi tristique dictum praesent natoque sem mi egestas venenatis per dui sit sodales est condimentum habitasse ipsum phasellus non bibendum hendrerit.

Function chr takes one argument or two arguments.
chr with one argument takes an integer between 0 and 255, and generates a single byte with the value specified by the integer.
chr with two arguments takes two integers between 0 and 255, and generates a byte sequence with the values specified by the integers.
For example #e#chr(0x41,0x45) generates data ABCDE.

Function repeat takes two arguments: an integer (with value 1 or more) and a byte sequence. This byte sequence can be a quoted string of characters (single quotes), like 'ABCDE' or an hexadecimal string prefixed with 0x, like 0x4142434445.
The repeat function will create a sequence of bytes consisting of the provided byte sequence (the second argument) repeated as many times as specified by the first argument.
For example, #e#repeat(3, 'AB') generates byte sequence ABABAB.

When more than one function needs to be used, the byte sequences generated by the functions can be concatenated with the + operator.
For example, #e#repeat(10,0xFF)+random(100) will generate a byte sequence of 10 FF bytes followed by 100 random bytes.

The cut argument (or cut operator) allows for the partial selection of the content of a file. This argument starts with #c# followed by a "cut-expression". Use this expression to "cut out" part of the content.
The cut-argument must be put in front of a file argument, like in this example:

tool.py #c#0:100l data.bin

With these arguments, tool.py will only process the first 100 bytes (0:100l) of file data.bin.

A cut argument is applied to all file arguments that follow it. Example:

tool.py #c#0:100l data-1.bin data-2.bin

With these arguments, tool.py will only process the first 100 bytes (0:100l) of file data-1.bin and the first 100 bytes file data-2.bin.

More than one cut argument can be used, like in this example:

tool.py #c#0:100l data-1.bin #c#0:200l data-2.bin

With these arguments, tool.py will only process the first 100 bytes (0:100l) of file data-1.bin and the first 200 bytes (0:200l) of file data-2.bin.

A cut-expression is composed of 2 terms separated by a colon (:), like this:
termA:termB
termA and termB can be:
- nothing (an empty string)
- a positive decimal number; example: 10
- an hexadecimal number (to be preceded by 0x); example: 0x10
- a case sensitive ASCII string to search for (surrounded by square brackets and single quotes); example: ['MZ']
- a case sensitive UNICODE string to search for (surrounded by square brackets and single quotes prefixed with u); example: [u'User']
- an hexadecimal string to search for (surrounded by square brackets); example: [d0cf11e0]
If termA is nothing, then the cut section of bytes starts with the byte at position 0.
If termA is a number, then the cut section of bytes starts with the byte at the position given by the number (first byte has index 0).
If termA is a string to search for, then the cut section of bytes starts with the byte at the position where the string is first found. If the string is not found, the cut is empty (0 bytes).
If termB is nothing, then the cut section of bytes ends with the last byte.
If termB is a number, then the cut section of bytes ends with the byte at the position given by the number (first byte has index 0).
When termB is a number, it can have suffix letter l. This indicates that the number is a length (number of bytes), and not a position.
termB can also be a negative number (decimal or hexademical): in that case the position is counted from the end of the file. For example, :-5 selects the complete file except the last 5 bytes.
If termB is a string to search for, then the cut section of bytes ends with the last byte at the position where the string is first found. If the string is not found, the cut is empty (0 bytes).
No checks are made to assure that the position specified by termA is lower than the position specified by termB. This is left up to the user.
Search string expressions (ASCII, UNICODE and hexadecimal) can be followed by an instance (a number equal to 1 or greater) to indicate which instance needs to be taken. For example, ['ABC']2 will search for the second instance of string 'ABC'. If this instance is not found, then nothing is selected.
Search string expressions (ASCII, UNICODE and hexadecimal) can be followed by an offset (+ or - a number) to add (or substract) an offset to the found instance. This number can be a decimal or hexadecimal (prefix 0x) value. For example, ['ABC']+3 will search for the first instance of string 'ABC' and then select the bytes after ABC (+ 3).
Finally, search string expressions (ASCII, UNICODE and hexadecimal) can be followed by an instance and an offset.
Examples:
This cut-expression can be used to dump the first 256 bytes of a PE file located inside the file content: ['MZ']:0x100l
This cut-expression can be used to dump the OLE file located inside the file content: [d0cf11e0]:

'''
    for line in manual.split('\n'):
        print(textwrap.fill(line, 79))

#Convert 2 Bytes If Python 3
def C2BIP3(string):
    if sys.version_info[0] > 2:
        return bytes([ord(x) for x in string])
    else:
        return string

#Convert 2 Integer If Python 2
def C2IIP2(data):
    if sys.version_info[0] > 2:
        return data
    else:
        return ord(data)

# CIC: Call If Callable
def CIC(expression):
    if callable(expression):
        return expression()
    else:
        return expression

# IFF: IF Function
def IFF(expression, valueTrue, valueFalse):
    if expression:
        return CIC(valueTrue)
    else:
        return CIC(valueFalse)

def LoremIpsumSentence(minimum, maximum):
    words = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'etiam', 'tortor', 'metus', 'cursus', 'sed', 'sollicitudin', 'ac', 'sagittis', 'eget', 'massa', 'praesent', 'sem', 'fermentum', 'dignissim', 'in', 'vel', 'augue', 'scelerisque', 'auctor', 'libero', 'nam', 'a', 'gravida', 'odio', 'duis', 'vestibulum', 'vulputate', 'quam', 'nec', 'cras', 'nibh', 'feugiat', 'ut', 'vitae', 'ornare', 'justo', 'orci', 'varius', 'natoque', 'penatibus', 'et', 'magnis', 'dis', 'parturient', 'montes', 'nascetur', 'ridiculus', 'mus', 'curabitur', 'nisl', 'egestas', 'urna', 'iaculis', 'lectus', 'maecenas', 'ultrices', 'velit', 'eu', 'porta', 'hac', 'habitasse', 'platea', 'dictumst', 'integer', 'id', 'commodo', 'mauris', 'interdum', 'malesuada', 'fames', 'ante', 'primis', 'faucibus', 'accumsan', 'pharetra', 'aliquam', 'nunc', 'at', 'est', 'non', 'leo', 'nulla', 'sodales', 'porttitor', 'facilisis', 'aenean', 'condimentum', 'rutrum', 'facilisi', 'tincidunt', 'laoreet', 'ultricies', 'neque', 'diam', 'euismod', 'consequat', 'tempor', 'elementum', 'lobortis', 'erat', 'ligula', 'risus', 'donec', 'phasellus', 'quisque', 'vivamus', 'pellentesque', 'tristique', 'venenatis', 'purus', 'mi', 'dictum', 'posuere', 'fringilla', 'quis', 'magna', 'pretium', 'felis', 'pulvinar', 'lacinia', 'proin', 'viverra', 'lacus', 'suscipit', 'aliquet', 'dui', 'molestie', 'dapibus', 'mollis', 'suspendisse', 'sapien', 'blandit', 'morbi', 'tellus', 'enim', 'maximus', 'semper', 'arcu', 'bibendum', 'convallis', 'hendrerit', 'imperdiet', 'finibus', 'fusce', 'congue', 'ullamcorper', 'placerat', 'nullam', 'eros', 'habitant', 'senectus', 'netus', 'turpis', 'luctus', 'volutpat', 'rhoncus', 'mattis', 'nisi', 'ex', 'tempus', 'eleifend', 'vehicula', 'class', 'aptent', 'taciti', 'sociosqu', 'ad', 'litora', 'torquent', 'per', 'conubia', 'nostra', 'inceptos', 'himenaeos']
    sample = random.sample(words, random.randint(minimum, maximum))
    sample[0] = sample[0].capitalize()
    return ' '.join(sample) + '.'

def LoremIpsum(sentences):
    return ' '.join([LoremIpsumSentence(15, 30) for i in range(sentences)])

STATE_START = 0
STATE_IDENTIFIER = 1
STATE_STRING = 2
STATE_SPECIAL_CHAR = 3
STATE_ERROR = 4

FUNCTIONNAME_REPEAT = 'repeat'
FUNCTIONNAME_RANDOM = 'random'
FUNCTIONNAME_CHR = 'chr'
FUNCTIONNAME_LOREMIPSUM = 'loremipsum'

def Tokenize(expression):
    result = []
    token = ''
    state = STATE_START
    while expression != '':
        char = expression[0]
        expression = expression[1:]
        if char == "'":
            if state == STATE_START:
                state = STATE_STRING
            elif state == STATE_IDENTIFIER:
                result.append([STATE_IDENTIFIER, token])
                state = STATE_STRING
                token = ''
            elif state == STATE_STRING:
                result.append([STATE_STRING, token])
                state = STATE_START
                token = ''
        elif char >= '0' and char <= '9' or char.lower() >= 'a' and char.lower() <= 'z':
            if state == STATE_START:
                token = char
                state = STATE_IDENTIFIER
            else:
                token += char
        elif char == ' ':
            if state == STATE_IDENTIFIER:
                result.append([STATE_IDENTIFIER, token])
                token = ''
                state = STATE_START
            elif state == STATE_STRING:
                token += char
        else:
            if state == STATE_IDENTIFIER:
                result.append([STATE_IDENTIFIER, token])
                token = ''
                state = STATE_START
                result.append([STATE_SPECIAL_CHAR, char])
            elif state == STATE_STRING:
                token += char
            else:
                result.append([STATE_SPECIAL_CHAR, char])
                token = ''
    if state == STATE_IDENTIFIER:
        result.append([state, token])
    elif state == STATE_STRING:
        result = [[STATE_ERROR, 'Error: string not closed', token]]
    return result

def ParseFunction(tokens):
    if len(tokens) == 0:
        print('Parsing error')
        return None, tokens
    if tokens[0][0] == STATE_STRING or tokens[0][0] == STATE_IDENTIFIER and tokens[0][1].startswith('0x'):
        return [[FUNCTIONNAME_REPEAT, [[STATE_IDENTIFIER, '1'], tokens[0]]], tokens[1:]]
    if tokens[0][0] != STATE_IDENTIFIER:
        print('Parsing error')
        return None, tokens
    function = tokens[0][1]
    tokens = tokens[1:]
    if len(tokens) == 0:
        print('Parsing error')
        return None, tokens
    if tokens[0][0] != STATE_SPECIAL_CHAR or tokens[0][1] != '(':
        print('Parsing error')
        return None, tokens
    tokens = tokens[1:]
    if len(tokens) == 0:
        print('Parsing error')
        return None, tokens
    arguments = []
    while True:
        if tokens[0][0] != STATE_IDENTIFIER and tokens[0][0] != STATE_STRING:
            print('Parsing error')
            return None, tokens
        arguments.append(tokens[0])
        tokens = tokens[1:]
        if len(tokens) == 0:
            print('Parsing error')
            return None, tokens
        if tokens[0][0] != STATE_SPECIAL_CHAR or (tokens[0][1] != ',' and tokens[0][1] != ')'):
            print('Parsing error')
            return None, tokens
        if tokens[0][0] == STATE_SPECIAL_CHAR and tokens[0][1] == ')':
            tokens = tokens[1:]
            break
        tokens = tokens[1:]
        if len(tokens) == 0:
            print('Parsing error')
            return None, tokens
    return [[function, arguments], tokens]

def Parse(expression):
    tokens = Tokenize(expression)
    if len(tokens) == 0:
        print('Parsing error')
        return None
    if tokens[0][0] == STATE_ERROR:
        print(tokens[0][1])
        print(tokens[0][2])
        print(expression)
        return None
    functioncalls = []
    while True:
        functioncall, tokens = ParseFunction(tokens)
        if functioncall == None:
            return None
        functioncalls.append(functioncall)
        if len(tokens) == 0:
            return functioncalls
        if tokens[0][0] != STATE_SPECIAL_CHAR or tokens[0][1] != '+':
            print('Parsing error')
            return None
        tokens = tokens[1:]

def InterpretInteger(token):
    if token[0] != STATE_IDENTIFIER:
        return None
    try:
        return int(token[1])
    except:
        return None

def Hex2Bytes(hexadecimal):
    if len(hexadecimal) % 2 == 1:
        hexadecimal = '0' + hexadecimal
    try:
        return binascii.a2b_hex(hexadecimal)
    except:
        return None

def InterpretHexInteger(token):
    if token[0] != STATE_IDENTIFIER:
        return None
    if not token[1].startswith('0x'):
        return None
    bytes = Hex2Bytes(token[1][2:])
    if bytes == None:
        return None
    integer = 0
    for byte in bytes:
        integer = integer * 0x100 + C2IIP2(byte)
    return integer

def InterpretNumber(token):
    number = InterpretInteger(token)
    if number == None:
        return InterpretHexInteger(token)
    else:
        return number

def InterpretBytes(token):
    if token[0] == STATE_STRING:
        return token[1]
    if token[0] != STATE_IDENTIFIER:
        return None
    if not token[1].startswith('0x'):
        return None
    return Hex2Bytes(token[1][2:])

def CheckFunction(functionname, arguments, countarguments, maxcountarguments=None):
    if maxcountarguments == None:
        if countarguments == 0 and len(arguments) != 0:
            print('Error: function %s takes no arguments, %d are given' % (functionname, len(arguments)))
            return True
        if countarguments == 1 and len(arguments) != 1:
            print('Error: function %s takes 1 argument, %d are given' % (functionname, len(arguments)))
            return True
        if countarguments != len(arguments):
            print('Error: function %s takes %d arguments, %d are given' % (functionname, countarguments, len(arguments)))
            return True
    else:
        if len(arguments) < countarguments or len(arguments) > maxcountarguments:
            print('Error: function %s takes between %d and %d arguments, %d are given' % (functionname, countarguments, maxcountarguments, len(arguments)))
            return True
    return False

def CheckNumber(argument, minimum=None, maximum=None):
    number = InterpretNumber(argument)
    if number == None:
        print('Error: argument should be a number: %s' % argument[1])
        return None
    if minimum != None and number < minimum:
        print('Error: argument should be minimum %d: %d' % (minimum, number))
        return None
    if maximum != None and number > maximum:
        print('Error: argument should be maximum %d: %d' % (maximum, number))
        return None
    return number

def Interpret(expression):
    functioncalls = Parse(expression)
    if functioncalls == None:
        return None
    decoded = ''
    for functioncall in functioncalls:
        functionname, arguments = functioncall
        if functionname == FUNCTIONNAME_REPEAT:
            if CheckFunction(functionname, arguments, 2):
                return None
            number = CheckNumber(arguments[0], minimum=1)
            if number == None:
                return None
            bytes = InterpretBytes(arguments[1])
            if bytes == None:
                print('Error: argument should be a byte sequence: %s' % arguments[1][1])
                return None
            decoded += number * bytes
        elif functionname == FUNCTIONNAME_RANDOM:
            if CheckFunction(functionname, arguments, 1):
                return None
            number = CheckNumber(arguments[0], minimum=1)
            if number == None:
                return None
            decoded += ''.join([chr(random.randint(0, 255)) for x in range(number)])
        elif functionname == FUNCTIONNAME_LOREMIPSUM:
            if CheckFunction(functionname, arguments, 1):
                return None
            number = CheckNumber(arguments[0], minimum=1)
            if number == None:
                return None
            decoded += LoremIpsum(number)
        elif functionname == FUNCTIONNAME_CHR:
            if CheckFunction(functionname, arguments, 1, 2):
                return None
            number = CheckNumber(arguments[0], minimum=1, maximum=255)
            if number == None:
                return None
            if len(arguments) == 1:
                decoded += chr(number)
            else:
                number2 = CheckNumber(arguments[1], minimum=1, maximum=255)
                if number2 == None:
                    return None
                decoded += ''.join([chr(n) for n in range(number, number2 + 1)])
        else:
            print('Error: unknown function: %s' % functionname)
            return None
    return decoded

FCH_FILENAME = 0
FCH_DATA = 1
FCH_ERROR = 2

def FilenameCheckHash(filename, literalfilename):
    if literalfilename:
        return FCH_FILENAME, filename
    elif filename.startswith('#h#'):
        result = Hex2Bytes(filename[3:])
        if result == None:
            return FCH_ERROR, 'hexadecimal'
        else:
            return FCH_DATA, result
    elif filename.startswith('#b#'):
        try:
            return FCH_DATA, binascii.a2b_base64(filename[3:])
        except:
            return FCH_ERROR, 'base64'
    elif filename.startswith('#e#'):
        result = Interpret(filename[3:])
        if result == None:
            return FCH_ERROR, 'expression'
        else:
            return FCH_DATA, result
    elif filename.startswith('#'):
        return FCH_DATA, C2BIP3(filename[1:])
    else:
        return FCH_FILENAME, filename

class cBinaryFile:
    def __init__(self, filename, zippassword='infected', noextraction=False, literalfilename=False):
        self.filename = filename
        self.zippassword = zippassword
        self.noextraction = noextraction
        self.literalfilename = literalfilename
        self.oZipfile = None
        self.extracted = False

        fch, data = FilenameCheckHash(self.filename, self.literalfilename)
        if fch == FCH_ERROR:
            raise Exception('Error %s parsing filename: %s' % (data, self.filename))

        if self.filename == '':
            if sys.platform == 'win32':
                import msvcrt
                msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
            self.fIn = sys.stdin
        elif fch == FCH_DATA:
            self.fIn = DataIO(data)
        elif not self.noextraction and self.filename.lower().endswith('.zip'):
            self.oZipfile = zipfile.ZipFile(self.filename, 'r')
            if len(self.oZipfile.infolist()) == 1:
                self.fIn = self.oZipfile.open(self.oZipfile.infolist()[0], 'r', self.zippassword)
                self.extracted = True
            else:
                self.oZipfile.close()
                self.oZipfile = None
                self.fIn = open(self.filename, 'rb')
        elif not self.noextraction and self.filename.lower().endswith('.gz'):
            self.fIn = gzip.GzipFile(self.filename, 'rb')
            self.extracted = True
        else:
            self.fIn = open(self.filename, 'rb')

    def close(self):
        if self.fIn != sys.stdin:
            self.fIn.close()
        if self.oZipfile != None:
            self.oZipfile.close()

    def read(self, size=None):
        try:
            fRead = self.fIn.buffer
        except:
            fRead = self.fIn
        if size == None:
            return fRead.read()
        else:
            return fRead.read(size)

    def Data(self):
        data = self.fIn.read()
        self.close()
        return data

def File2Strings(filename):
    try:
        if filename == '':
            f = sys.stdin
        else:
            f = open(filename, 'r')
    except:
        return None
    try:
        return map(lambda line:line.rstrip('\n'), f.readlines())
    except:
        return None
    finally:
        if f != sys.stdin:
            f.close()

def ProcessAt(argument):
    if argument.startswith('@'):
        strings = File2Strings(argument[1:])
        if strings == None:
            raise Exception('Error reading %s' % argument)
        else:
            return strings
    else:
        return [argument]

def Glob(filename):
    filenames = glob.glob(filename)
    if len(filenames) == 0:
        return [filename]
    else:
        return filenames

def ExpandFilenameArguments(filenames, literalfilenames=False):
    if len(filenames) == 0:
        return [['', '']]
    elif literalfilenames:
        return [[filename, ''] for filename in filenames]
    else:
        cutexpression = ''
        result = []
        for filename in list(collections.OrderedDict.fromkeys(sum(map(Glob, sum(map(ProcessAt, filenames), [])), []))):
            if filename.startswith('#c#'):
                cutexpression = filename[3:]
            else:
                result.append([filename, cutexpression])
        if result == []:
            return [['', cutexpression]]
        return result

CUTTERM_NOTHING = 0
CUTTERM_POSITION = 1
CUTTERM_FIND = 2
CUTTERM_LENGTH = 3

def Replace(string, dReplacements):
    if string in dReplacements:
        return dReplacements[string]
    else:
        return string

def ParseInteger(argument):
    sign = 1
    if argument.startswith('+'):
        argument = argument[1:]
    elif argument.startswith('-'):
        argument = argument[1:]
        sign = -1
    if argument.startswith('0x'):
        return sign * int(argument[2:], 16)
    else:
        return sign * int(argument)

def ParseCutTerm(argument):
    if argument == '':
        return CUTTERM_NOTHING, None, ''
    oMatch = re.match(r'\-?0x([0-9a-f]+)', argument, re.I)
    if oMatch == None:
        oMatch = re.match(r'\-?(\d+)', argument)
    else:
        value = int(oMatch.group(1), 16)
        if argument.startswith('-'):
            value = -value
        return CUTTERM_POSITION, value, argument[len(oMatch.group(0)):]
    if oMatch == None:
        oMatch = re.match(r'\[([0-9a-f]+)\](\d+)?([+-](?:0x[0-9a-f]+|\d+))?', argument, re.I)
    else:
        value = int(oMatch.group(1))
        if argument.startswith('-'):
            value = -value
        return CUTTERM_POSITION, value, argument[len(oMatch.group(0)):]
    if oMatch == None:
        oMatch = re.match(r"\[u?\'(.+?)\'\](\d+)?([+-](?:0x[0-9a-f]+|\d+))?", argument)
    else:
        if len(oMatch.group(1)) % 2 == 1:
            raise Exception("Uneven length hexadecimal string")
        else:
            return CUTTERM_FIND, (binascii.a2b_hex(oMatch.group(1)), int(Replace(oMatch.group(2), {None: '1'})), ParseInteger(Replace(oMatch.group(3), {None: '0'}))), argument[len(oMatch.group(0)):]
    if oMatch == None:
        return None, None, argument
    else:
        if argument.startswith("[u'"):
            # convert ascii to unicode 16 byte sequence
            searchtext = oMatch.group(1).decode('unicode_escape').encode('utf16')[2:]
        else:
            searchtext = oMatch.group(1)
        return CUTTERM_FIND, (searchtext, int(Replace(oMatch.group(2), {None: '1'})), ParseInteger(Replace(oMatch.group(3), {None: '0'}))), argument[len(oMatch.group(0)):]

def ParseCutArgument(argument):
    type, value, remainder = ParseCutTerm(argument.strip())
    if type == CUTTERM_NOTHING:
        return CUTTERM_NOTHING, None, CUTTERM_NOTHING, None
    elif type == None:
        if remainder.startswith(':'):
            typeLeft = CUTTERM_NOTHING
            valueLeft = None
            remainder = remainder[1:]
        else:
            return None, None, None, None
    else:
        typeLeft = type
        valueLeft = value
        if typeLeft == CUTTERM_POSITION and valueLeft < 0:
            return None, None, None, None
        if typeLeft == CUTTERM_FIND and valueLeft[1] == 0:
            return None, None, None, None
        if remainder.startswith(':'):
            remainder = remainder[1:]
        else:
            return None, None, None, None
    type, value, remainder = ParseCutTerm(remainder)
    if type == CUTTERM_POSITION and remainder == 'l':
        return typeLeft, valueLeft, CUTTERM_LENGTH, value
    elif type == None or remainder != '':
        return None, None, None, None
    elif type == CUTTERM_FIND and value[1] == 0:
        return None, None, None, None
    else:
        return typeLeft, valueLeft, type, value

def Find(data, value, nth, startposition=-1):
    position = startposition
    while nth > 0:
        position = data.find(value, position + 1)
        if position == -1:
            return -1
        nth -= 1
    return position

def CutData(stream, cutArgument):
    if cutArgument == '':
        return [stream, None, None]

    typeLeft, valueLeft, typeRight, valueRight = ParseCutArgument(cutArgument)

    if typeLeft == None:
        return [stream, None, None]

    if typeLeft == CUTTERM_NOTHING:
        positionBegin = 0
    elif typeLeft == CUTTERM_POSITION:
        positionBegin = valueLeft
    elif typeLeft == CUTTERM_FIND:
        positionBegin = Find(stream, valueLeft[0], valueLeft[1])
        if positionBegin == -1:
            return ['', None, None]
        positionBegin += valueLeft[2]
    else:
        raise Exception("Unknown value typeLeft")

    if typeRight == CUTTERM_NOTHING:
        positionEnd = len(stream)
    elif typeRight == CUTTERM_POSITION and valueRight < 0:
        positionEnd = len(stream) + valueRight
    elif typeRight == CUTTERM_POSITION:
        positionEnd = valueRight + 1
    elif typeRight == CUTTERM_LENGTH:
        positionEnd = positionBegin + valueRight
    elif typeRight == CUTTERM_FIND:
        positionEnd = Find(stream, valueRight[0], valueRight[1], positionBegin)
        if positionEnd == -1:
            return ['', None, None]
        else:
            positionEnd += len(valueRight[0])
        positionEnd += valueRight[2]
    else:
        raise Exception("Unknown value typeRight")

    return [stream[positionBegin:positionEnd], positionBegin, positionEnd]

class cDump():
    def __init__(self, data, prefix='', offset=0, dumplinelength=16):
        self.data = data
        self.prefix = prefix
        self.offset = offset
        self.dumplinelength = dumplinelength

    def HexDump(self):
        oDumpStream = self.cDumpStream(self.prefix)
        hexDump = ''
        for i, b in enumerate(self.data):
            if i % self.dumplinelength == 0 and hexDump != '':
                oDumpStream.Addline(hexDump)
                hexDump = ''
            hexDump += IFF(hexDump == '', '', ' ') + '%02X' % self.C2IIP2(b)
        oDumpStream.Addline(hexDump)
        return oDumpStream.Content()

    def CombineHexAscii(self, hexDump, asciiDump):
        if hexDump == '':
            return ''
        countSpaces = 3 * (self.dumplinelength - len(asciiDump))
        if len(asciiDump) <= self.dumplinelength / 2:
            countSpaces += 1
        return hexDump + '  ' + (' ' * countSpaces) + asciiDump

    def HexAsciiDump(self, rle=False):
        oDumpStream = self.cDumpStream(self.prefix)
        position = ''
        hexDump = ''
        asciiDump = ''
        previousLine = None
        countRLE = 0
        for i, b in enumerate(self.data):
            b = self.C2IIP2(b)
            if i % self.dumplinelength == 0:
                if hexDump != '':
                    line = self.CombineHexAscii(hexDump, asciiDump)
                    if not rle or line != previousLine:
                        if countRLE > 0:
                            oDumpStream.Addline('* %d 0x%02x' % (countRLE, countRLE * self.dumplinelength))
                        oDumpStream.Addline(position + line)
                        countRLE = 0
                    else:
                        countRLE += 1
                    previousLine = line
                position = '%08X:' % (i + self.offset)
                hexDump = ''
                asciiDump = ''
            if i % self.dumplinelength == self.dumplinelength / 2:
                hexDump += ' '
            hexDump += ' %02X' % b
            asciiDump += IFF(b >= 32 and b < 128, chr(b), '.')
        if countRLE > 0:
            oDumpStream.Addline('* %d 0x%02x' % (countRLE, countRLE * self.dumplinelength))
        oDumpStream.Addline(self.CombineHexAscii(position + hexDump, asciiDump))
        return oDumpStream.Content()

    def Base64Dump(self, nowhitespace=False):
        encoded = binascii.b2a_base64(self.data)
        if nowhitespace:
            return encoded
        encoded = encoded.strip()
        oDumpStream = self.cDumpStream(self.prefix)
        length = 64
        for i in range(0, len(encoded), length):
            oDumpStream.Addline(encoded[0+i:length+i])
        return oDumpStream.Content()

    class cDumpStream():
        def __init__(self, prefix=''):
            self.oStringIO = StringIO()
            self.prefix = prefix

        def Addline(self, line):
            if line != '':
                self.oStringIO.write(self.prefix + line + '\n')

        def Content(self):
            return self.oStringIO.getvalue()

    @staticmethod
    def C2IIP2(data):
        if sys.version_info[0] > 2:
            return data
        else:
            return ord(data)

def IfWIN32SetBinary(io):
    if sys.platform == 'win32':
        import msvcrt
        msvcrt.setmode(io.fileno(), os.O_BINARY)

#Fix for http://bugs.python.org/issue11395
def StdoutWriteChunked(data):
    if sys.version_info[0] > 2:
        sys.stdout.buffer.write(data)
    else:
        while data != '':
            sys.stdout.write(data[0:10000])
            try:
                sys.stdout.flush()
            except IOError:
                return
            data = data[10000:]

class cOutput():
    def __init__(self, options=None):
        self.options = options
        self.lines = []

    def PrintC(self, line):
        if self.options == None or self.options.select == '':
            self.Print(line)

    def Print(self, line):
        self.lines.append(line)

    def Output(self):
        for line in self.lines:
            print(line)

def Print(line, options=None):
    if options == None or options.select == '':
        print(line)

def CalculateByteStatistics(dPrevalence=None, data=None):
    averageConsecutiveByteDifference = None
    if dPrevalence == None:
        dPrevalence = {iter: 0 for iter in range(0x100)}
        sumDifferences = 0.0
        previous = None
        if len(data) > 1:
            for byte in data:
                byte = C2IIP2(byte)
                dPrevalence[byte] += 1
                if previous != None:
                    sumDifferences += abs(byte - previous)
                previous = byte
            averageConsecutiveByteDifference = sumDifferences /float(len(data)-1)
    sumValues = sum(dPrevalence.values())
    countNullByte = dPrevalence[0]
    countControlBytes = 0
    countWhitespaceBytes = 0
    countUniqueBytes = 0
    for iter in range(1, 0x21):
        if chr(iter) in string.whitespace:
            countWhitespaceBytes += dPrevalence[iter]
        else:
            countControlBytes += dPrevalence[iter]
    countControlBytes += dPrevalence[0x7F]
    countPrintableBytes = 0
    for iter in range(0x21, 0x7F):
        countPrintableBytes += dPrevalence[iter]
    countHighBytes = 0
    for iter in range(0x80, 0x100):
        countHighBytes += dPrevalence[iter]
    countHexadecimalBytes = 0
    countBASE64Bytes = 0
    for iter in range(0x30, 0x3A):
        countHexadecimalBytes += dPrevalence[iter]
        countBASE64Bytes += dPrevalence[iter]
    for iter in range(0x41, 0x47):
        countHexadecimalBytes += dPrevalence[iter]
    for iter in range(0x61, 0x67):
        countHexadecimalBytes += dPrevalence[iter]
    for iter in range(0x41, 0x5B):
        countBASE64Bytes += dPrevalence[iter]
    for iter in range(0x61, 0x7B):
        countBASE64Bytes += dPrevalence[iter]
    countBASE64Bytes += dPrevalence[ord('+')] + dPrevalence[ord('/')] + dPrevalence[ord('=')]
    entropy = 0.0
    for iter in range(0x100):
        if dPrevalence[iter] > 0:
            prevalence = float(dPrevalence[iter]) / float(sumValues)
            entropy += - prevalence * math.log(prevalence, 2)
            countUniqueBytes += 1
    return sumValues, entropy, countUniqueBytes, countNullByte, countControlBytes, countWhitespaceBytes, countPrintableBytes, countHighBytes, countHexadecimalBytes, countBASE64Bytes, averageConsecutiveByteDifference

def GetMarkerName(markerType):
    dMarkers = {0xD8:'SOI', 0xD9:'EOI', 0xDA:'SOS', 0xDB:'DQT', 0xC0:'SOF0', 0xC2:'SOF2', 0xC4:'DHT', 0xDD:'DRI', 0xFE:'COM'}
    if markerType in dMarkers:
        return dMarkers[markerType]
    if markerType >= 0xE0 and markerType <= 0xEF:
        return 'APP%d' % (markerType & 0x0F)
    if markerType >= 0xD0 and markerType <= 0xD7:
        return 'RST%d' % (markerType & 0x0F)
    return ''

def ConvertNone(value, replacement=''):
    if value == None:
        return replacement
    else:
        return value

def GetDelta(found, endOfPrevious, noPrevious):
    if noPrevious:
        return '   '
    else:
        return 'd=%d' % (found - endOfPrevious)

def PrintWarningSelection(select, selectionCounter):
    if select != '' and selectionCounter == 0:
        print('Warning: no segment was selected with expression %s' % select)

def ProcessJPEGFileSub(data, options, startposition=0):
    oOutput = cOutput(options)
    counter = 1
    selectionCounterTotal = 0
    position = startposition
    ff00Counter = 0
    ffdaFlag = False
    endOfPrevious = startposition
    noPrevious = True
    positionSOI = None
    positionEOI = None
    while True:
        found = data.find(C2BIP3('\xFF'), position)
        if found == -1 or positionEOI != None and options.trailing:
            break
        marker = data[found:found+4]
        markerType = struct.unpack('xB', marker[0:2])[0]
        markerName = GetMarkerName(markerType)
        if markerType == 0x00:
            if ffdaFlag:
                ff00Counter += 1
            position = found + 1
        elif ffdaFlag and markerType >= 0xD0 and markerType <= 0xD7:
            oOutput.PrintC('    p=0x%08x    : m=%02x%02x %s' % (found, struct.unpack('Bx', marker[0:2])[0], markerType, markerName))
            position = found + 1
        else:
            if ffdaFlag:
                stats = CalculateByteStatistics(data=data[endOfPrevious:found])
                oOutput.PrintC('                                  entropy-coded data: l=%d e=%f a=%f #ff00=%d' % ((found - endOfPrevious), stats[1], ConvertNone(stats[10], 0.0), ff00Counter))
                ff00Counter = 0
                if options.select == '%di' % (counter - 1):
                    selectionCounterTotal += 1
                    if options.dump:
                        IfWIN32SetBinary(sys.stdout)
                        StdoutWriteChunked(data[endOfPrevious:found])
                    elif options.unstuffeddump:
                        IfWIN32SetBinary(sys.stdout)
                        StdoutWriteChunked(data[endOfPrevious:found].replace(C2BIP3('\xFF\x00'), C2BIP3('\xFF')))
                    elif options.hexdump:
                        oOutput.Print(cDump(data[endOfPrevious:found], '      ', endOfPrevious).HexDump()[:-1])
                    elif options.asciidumprle:
                        oOutput.Print(cDump(data[endOfPrevious:found], '      ', endOfPrevious).HexAsciiDump(True)[:-1])
                    else:
                        oOutput.Print(cDump(data[endOfPrevious:found], '      ', endOfPrevious).HexAsciiDump()[:-1])
                endOfPrevious = found
                noPrevious = False
            if markerType == 0xda:
                ffdaFlag = True
            else:
                ffdaFlag = False
            if options.findsoi and options.compliant and noPrevious == False and endOfPrevious != found:
                return None
            if markerType >= 0xD0 and markerType <= 0xD9:
                if positionSOI == None and markerType == 0xD8:
                    positionSOI = found
                if positionEOI == None and markerType == 0xD9:
                    positionEOI = found
                oOutput.PrintC('%3d p=0x%08x %s: m=%02x%02x %s' % (counter, found, GetDelta(found, endOfPrevious, noPrevious), struct.unpack('Bx', marker[0:2])[0], markerType, markerName))
                if options.findsoi and options.compliant and markerType == 0xD9:
                    return oOutput, positionSOI, found
                position = found + 2
                endOfPrevious = position
                noPrevious = False
                counter += 1
            else:
                size = struct.unpack('>xxH', marker)[0]
                stats = CalculateByteStatistics(data=data[found+4:found+2+size])
                line = '%3d p=0x%08x %s: m=%02x%02x %-5s l=%5d e=%f a=%f' % (counter, found, GetDelta(found, endOfPrevious, noPrevious), struct.unpack('Bx', marker[0:2])[0], markerType, markerName, size, stats[1], ConvertNone(stats[10], 0.0))
                # http://vip.sugovica.hu/Sardi/kepnezo/JPEG%20File%20Layout%20and%20Format.htm
                if markerType == 0xDB:
                    line += ' remark: %d/65 = %f' % (size - 2, (size - 2.0) / 65.0)
                if markerType == 0xC0 and len(data[found+4:found+2+size]) >= 6:
                    info = struct.unpack('>BHHB', data[found+4:found+2+size][:6])
                    line += ' remark: p=%d h=%d w=%d c=%d' % info
                if markerType == 0xDA and len(data[found+4:found+2+size]) >= 1:
                    info = struct.unpack('>B', data[found+4:found+2+size][:1])
                    line += ' remark: c=%d' % info
                oOutput.PrintC(line)
                if options.select == str(counter):
                    selectionCounterTotal += 1
                    if options.dump:
                        IfWIN32SetBinary(sys.stdout)
                        StdoutWriteChunked(data[found+4:found+2+size])
                    elif options.hexdump:
                        oOutput.Print(cDump(data[found+4:found+2+size], '', found + 4).HexDump()[:-1])
                    elif options.asciidumprle:
                        oOutput.Print(cDump(data[found+4:found+2+size], '', found + 4).HexAsciiDump(True)[:-1])
                    else:
                        oOutput.Print(cDump(data[found+4:found+2+size], '', found + 4).HexAsciiDump()[:-1])
                position = found + 2 + size
                endOfPrevious = position
                noPrevious = False
                counter += 1
        if options.findsoi and markerName == '' and markerType != 0x00:
            if options.compliant:
                return None
            else:
                return oOutput, positionSOI, None
        markerTypePrevious = markerType
    trailing = len(data) - position
    if trailing > 0:
        stats = CalculateByteStatistics(data=data[position:])
        oOutput.PrintC('%3d p=0x%08x    : *trailing*  l=%5d e=%f' % (counter, position, len(data) - position, stats[1]))
        if options.select == str(counter):
            selectionCounterTotal += 1
            if options.dump:
                IfWIN32SetBinary(sys.stdout)
                StdoutWriteChunked(data[position:])
            elif options.hexdump:
                oOutput.Print(cDump(data[position:], '', position).HexDump()[:-1])
            elif options.asciidumprle:
                oOutput.Print(cDump(data[position:], '', position).HexAsciiDump(True)[:-1])
            else:
                oOutput.Print(cDump(data[position:], '', position).HexAsciiDump()[:-1])
    elif trailing < 0:
        oOutput.PrintC('                                   *warning* %d byte(s) missing' % -trailing)
    PrintWarningSelection(options.select, selectionCounterTotal)
    return oOutput, None, None

def ProcessJPEGFile(filename, cutexpression, options):
    oBinaryFile = cBinaryFile(filename, C2BIP3(options.password), options.noextraction, options.literalfilenames)
    data = CutData(oBinaryFile.Data(), cutexpression)[0]
    Print('File: %s%s' % (filename, IFF(oBinaryFile.extracted, ' (extracted)', '')), options)
    if options.findsoi:
        position = 0
        while True:
            found = data.find(C2BIP3('\xFF\xD8'), position)
            if found == -1:
                break
            result = ProcessJPEGFileSub(data, options, found)
            if result != None:
                Print('Found SOI:', options)
                result[0].Output()
                if options.extract and result[1] != None and result[2] != None and result[2] - result[1] > 2:
                    open('%08x.jpeg' % result[1], 'wb').write(data[result[1]:result[2]+2])
            position = found + 1
    else:
        result = ProcessJPEGFileSub(data, options)
        if result != None:
            result[0].Output()

def ProcessJPEGFiles(filenames, options):
    for filename, cutexpression in filenames:
        ProcessJPEGFile(filename, cutexpression, options)

def Main():
    moredesc = '''

Source code put in the public domain by Didier Stevens, no Copyright
Use at your own risk
https://DidierStevens.com'''

    oParser = optparse.OptionParser(usage='usage: %prog [options] [file ...]\n' + __description__ + moredesc, version='%prog ' + __version__)
    oParser.add_option('-m', '--man', action='store_true', default=False, help='Print manual')
    oParser.add_option('--password', default='infected', help='The ZIP password to be used (default infected)')
    oParser.add_option('--noextraction', action='store_true', default=False, help='Do not extract from archive file')
    oParser.add_option('--literalfilenames', action='store_true', default=False, help='Do not interpret filenames')
    oParser.add_option('-s', '--select', default='', help='Select marker nr for dumping')
    oParser.add_option('-d', '--dump', action='store_true', default=False, help='Perform dump')
    oParser.add_option('-u', '--unstuffeddump', action='store_true', default=False, help='Perform unstuffed dump')
    oParser.add_option('-a', '--asciidump', action='store_true', default=False, help='Perform HEX/ASCII dump')
    oParser.add_option('-A', '--asciidumprle', action='store_true', default=False, help='perform ascii dump with RLE')
    oParser.add_option('-x', '--hexdump', action='store_true', default=False, help='Perform HEX dump')
    oParser.add_option('-f', '--findsoi', action='store_true', default=False, help='Find SOI markers')
    oParser.add_option('-c', '--compliant', action='store_true', default=False, help='Combined with --findsoi, report compliant images only')
    oParser.add_option('-e', '--extract', action='store_true', default=False, help='Combined with --findsoi, extract images to disk')
    oParser.add_option('-t', '--trailing', action='store_true', default=False, help='Consider everything after the first EOI as trailing')
    (options, args) = oParser.parse_args()

    if options.man:
        oParser.print_help()
        PrintManual()
        return

    ProcessJPEGFiles(ExpandFilenameArguments(args, options.literalfilenames), options)

if __name__ == '__main__':
    Main()
